import {
  ETerminalErrorType,
  ITerminalNodeService,
  ITerminalServiceClient,
  TERMINAL_ID_SEPARATOR,
} from '../common';
import { IPtyProcessProxy, IPtyService, IShellLaunchConfig } from '../common';

import { PtyService } from './pty';
import { isDevelopment, isElectronNode, pSeries } from '../../utils';
import { getChunks } from '../../utils/strings';
import { IPtyServiceManager } from './pty.manager';
import { ShellIntegrationService } from './shell-integration.service';

// ref: https://github.com/vercel/hyper/blob/4c90d7555c79fb6dc438fa9549f1d0ef7c7a5aa7/app/session.ts#L27-L32
// 批处理字符最大长度 (200KB)
const BATCH_MAX_SIZE = 200 * 1024;
// 批处理延时
const BATCH_DURATION_MS = 16;

/**
 * 每个通知前端的数据包最大的大小 (20MB)
 */
const BATCH_CHUNK_MAX_SIZE = 20 * 1024 * 1024;

// 终端服务实现，负责管理终端的创建、数据通信、关闭等
export class TerminalServiceImpl implements ITerminalNodeService {
  // 终端关闭的默认延迟阈值（10秒）
  static TerminalPtyCloseThreshold = 10 * 1000;

  // sessionId -> IPtyService
  private terminalProcessMap: Map<string, IPtyService> = new Map();
  // clientId -> (sessionId -> PtyService)
  private clientTerminalMap: Map<string, Map<string, PtyService>> = new Map();

  // clientId -> ITerminalServiceClient
  private serviceClientMap: Map<string, ITerminalServiceClient> = new Map();
  // clientId -> close 定时器
  private closeTimeOutMap: Map<string, NodeJS.Timeout> = new Map();

  // sessionId -> batched data
  private batchedPtyDataMap: Map<string, string> = new Map();
  // sessionId -> batched data timer
  private batchedPtyDataTimer: Map<string, NodeJS.Timeout> = new Map();

  constructor(
    private readonly ptyServiceManager: IPtyServiceManager,
    private readonly shellIntegrationService: ShellIntegrationService
  ) {}

  /**
   * 注册前端 client 到服务端
   * @param clientId 客户端唯一标识
   * @param client 客户端实例
   */
  public setClient(clientId: string, client: ITerminalServiceClient) {
    if (clientId.indexOf(TERMINAL_ID_SEPARATOR) >= 0) {
      clientId = clientId.split(TERMINAL_ID_SEPARATOR)[0];
    }
    this.serviceClientMap.set(clientId, client);
    // 如果有相同的setClient clientId被调用，则取消延时触发closeClient，否则会导致终端无响应
    const timeOutHandler = this.closeTimeOutMap.get(clientId);
    if (timeOutHandler) {
      clearTimeout(timeOutHandler);
      this.closeTimeOutMap.delete(clientId);
    }
  }

  /**
   * 检查 SessionId 是否存活
   * 由于接口设计问题，只能返回 boolean，不能批量返回
   * @param clientId 客户端 id
   * @param terminalIdArr 终端 id 数组
   */
  public async ensureClientTerminal(clientId: string, terminalIdArr: string[]) {
    const sessionIdArray = terminalIdArr.map(
      (id) => id.split(TERMINAL_ID_SEPARATOR)[1]
    );
    const sessionCheckResArray = await Promise.all(
      sessionIdArray.map((sessionId) =>
        this.ptyServiceManager.checkSession(sessionId)
      )
    );
    // log.log(
    //   `Ensure terminal client ${clientId} ${terminalIdArr} ${sessionCheckResArray}`
    // );

    // 有一个存活就返回 true
    for (const sessionCheckRes of sessionCheckResArray) {
      if (sessionCheckRes) {
        return true;
      }
    }
    return false;
  }

  /**
   * 延时关闭 client，避免因 WS 重连导致终端不可用
   * @param clientId 客户端 id
   */
  public closeClient(clientId: string) {
    // 延时触发，因为WS本身有重连逻辑，因此通过延时触发来避免断开后不就重连但是回调方法都被dispose的问题
    const closeTimer = setTimeout(
      () => {
        this.disposeClient(clientId);
        // log.debug(`Remove pty process from ${clientId} client`);
      },
      isDevelopment()
        ? 0
        : //this.appConfig.terminalPtyCloseThreshold ||
          TerminalServiceImpl.TerminalPtyCloseThreshold
    );
    this.closeTimeOutMap.set(clientId, closeTimer);
  }

  /**
   * 彻底销毁 client 及其所有终端
   * @param clientId 客户端 id
   */
  public disposeClient(clientId: string) {
    const terminalMap = this.clientTerminalMap.get(clientId);
    // 如果是Electron也要直接kill掉，跟随IDE Server的生命周期
    const isElectronNodeEnv = isElectronNode();

    if (terminalMap) {
      terminalMap.forEach((t, id) => {
        this.terminalProcessMap.delete(id);

        if (
          t.shellLaunchConfig.disablePersistence ||
          t.shellLaunchConfig.isExtensionOwnedTerminal ||
          isElectronNodeEnv
        ) {
          t.kill(); // shellLaunchConfig 有 isTransient 的参数时，要Kill，不保活
        }
        // t.kill(); // 这个是窗口关闭时候触发，终端默认在这种场景下保活, 不kill
        // TODO: 后续看看有没有更加优雅的方案
      });
      this.clientTerminalMap.delete(clientId);
    }
  }

  /**
   * 立即将 batched 的终端数据发送给前端
   * @param clientId 客户端 id
   * @param sessionId session id
   */
  private flushPtyData(clientId: string, sessionId: string) {
    if (!this.batchedPtyDataMap.has(sessionId)) {
      return;
    }

    const ptyData = this.batchedPtyDataMap.get(sessionId)!;
    this.batchedPtyDataMap.delete(sessionId);

    if (this.batchedPtyDataTimer.has(sessionId)) {
      clearTimeout(this.batchedPtyDataTimer.get(sessionId)!);
      this.batchedPtyDataTimer.delete(sessionId);
    }

    const serviceClient = this.serviceClientMap.get(
      clientId
    ) as ITerminalServiceClient;

    // 按最大包大小切分数据，逐个发送
    const chunks = getChunks(ptyData, BATCH_CHUNK_MAX_SIZE);
    pSeries(
      chunks.map((str) => () => serviceClient.clientMessage(sessionId, str))
    );
  }

  /**
   * 创建终端实例
   * @param sessionId session id
   * @param cols 列数
   * @param rows 行数
   * @param launchConfig 启动配置
   */
  public async create2(
    sessionId: string,
    cols: number,
    rows: number,
    launchConfig: IShellLaunchConfig
  ): Promise<IPtyProcessProxy | undefined> {
    const clientId = sessionId.split(TERMINAL_ID_SEPARATOR)[0];
    let ptyService: PtyService | undefined;

    try {
      ptyService = new PtyService(
        sessionId,
        launchConfig,
        cols,
        rows,
        this.ptyServiceManager,
        this.shellIntegrationService
      );

      // // 创建 PtyService 实例
      // ptyService = this.injector.get(PtyService, [
      //   sessionId,
      //   launchConfig,
      //   cols,
      //   rows,
      // ]);
      this.terminalProcessMap.set(sessionId, ptyService);

      // ref: https://hyper.is/blog
      // 合并 pty 输出的数据，16ms 后发送给客户端，如
      // 果在 16ms 内没有收到新的数据，或短时间内数据
      // 超过 BATCH_MAX_SIZE 限定的长度，则立即发送缓
      // 存的数据，避免因为输出较多时阻塞 RPC 通信

      // 合并 pty 输出的数据，16ms 后发送给客户端
      ptyService.onData((chunk: string) => {
        console.log('onData', sessionId, chunk.length, chunk);

        if (this.serviceClientMap.has(clientId)) {
          const ptyData = this.batchedPtyDataMap.get(sessionId) || '';

          this.batchedPtyDataMap.set(sessionId, ptyData + chunk);

          if (ptyData.length + chunk.length >= BATCH_MAX_SIZE) {
            this.flushPtyData(clientId, sessionId);
          }

          if (!this.batchedPtyDataTimer.has(sessionId)) {
            this.batchedPtyDataTimer.set(
              sessionId,
              setTimeout(
                () => this.flushPtyData(clientId, sessionId),
                BATCH_DURATION_MS
              )
            );
          }
        } else {
          console.warn(`terminal: pty ${clientId} on data not found`);
        }
      });

      // 监听终端退出事件
      ptyService.onExit(({ exitCode, signal }) => {
        // log.debug(`Terminal process ${sessionId} exit with code ${exitCode}`);
        if (this.serviceClientMap.has(clientId)) {
          this.flushPtyData(clientId, sessionId);
          const serviceClient = this.serviceClientMap.get(
            clientId
          ) as ITerminalServiceClient;
          serviceClient.closeClient(sessionId, {
            code: exitCode,
            signal,
          });
        } else {
          // log.warn(`The pty process ${clientId} not found`);
        }
      });

      // 监听进程名变更事件
      ptyService.onProcessChange((processName) => {
        // log.debug(`Terminal process change (${processName})`);
        if (this.serviceClientMap.has(clientId)) {
          const serviceClient = this.serviceClientMap.get(
            clientId
          ) as ITerminalServiceClient;
          serviceClient.processChange(sessionId, processName);
        } else {
          // log.warn(`The pty process ${clientId} not found`);
        }
      });

      // 启动终端
      const error = await ptyService.start();

      if (error) {
        console.error(`Terminal process ${sessionId} start error\n`, error);
        throw error;
      }

      // 记录 clientId 和 sessionId 的映射关系
      if (!this.clientTerminalMap.has(clientId)) {
        this.clientTerminalMap.set(clientId, new Map());
      }
      this.clientTerminalMap.get(clientId)?.set(sessionId, ptyService);
    } catch (error) {
      console.error(
        `${sessionId} create terminal error: ${JSON.stringify(
          error
        )}, options: ${JSON.stringify(launchConfig)}`
      );
      if (this.serviceClientMap.has(clientId)) {
        const serviceClient = this.serviceClientMap.get(
          clientId
        ) as ITerminalServiceClient;
        serviceClient.closeClient(sessionId, {
          id: sessionId,
          message: (error as any)?.message,
          type: ETerminalErrorType.CREATE_FAIL,
          stopped: true,
          launchConfig,
        });
      }
    }

    return ptyService?.pty;
  }

  /**
   * 处理前端发来的消息
   * @param id 终端 id
   * @param msg 消息内容
   */
  public onMessage(id: string, msg: string) {
    const terminal = this.getTerminal(id);
    if (!terminal) {
      // log.warn(`The terminal ${id} not found`);
      return;
    }
    terminal.onMessage(msg);
  }

  /**
   * 调整终端尺寸
   * @param id 终端 id
   * @param rows 行数
   * @param cols 列数
   */
  public resize(id: string, rows: number, cols: number) {
    const terminal = this.getTerminal(id);

    if (!terminal) {
      return;
    }
    terminal.resize(rows, cols);
  }

  /**
   * 获取终端 shell 名称
   * @param id 终端 id
   */
  getShellName(id: string): string {
    const terminal = this.getTerminal(id);
    if (!terminal) {
      return 'invalid terminal';
    }
    return terminal.getShellName();
  }

  /**
   * 获取终端进程 id
   * @param id 终端 id
   */
  getProcessId(id: string): number {
    const terminal = this.getTerminal(id);
    if (!terminal) {
      return -1;
    }
    return terminal.getPid();
  }

  /**
   * 销毁指定 id 的终端
   * @param id 终端 id
   */
  disposeById(id: string) {
    const terminal = this.getTerminal(id);

    if (!terminal) {
      return;
    }
    terminal.kill();
  }

  /**
   * 销毁所有终端（暂未实现，避免重连时终端不可用）
   */
  dispose() {
    // TODO 后续需要一个合理的 Dispose 逻辑，暂时不要 Dispose，避免重连时终端不可用
    // this.serviceClientMap.forEach((client) => {
    //   client.dispose();
    // });
  }

  /**
   * 获取指定 id 的终端实例
   * @param id 终端 id
   */
  private getTerminal(id: string) {
    return this.terminalProcessMap.get(id);
  }

  /**
   * 获取终端当前工作目录
   * @param id 终端 id
   */
  async getCwd(id: string): Promise<string | undefined> {
    const ptyService = this.getTerminal(id);
    if (!ptyService) {
      return undefined;
    }

    return await ptyService.getCwd();
  }
}
